package com.opennews.openplatform.myspringbootcore.security;

import com.opennews.openplatform.myspringbootcore.dto.RequestMap;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * Used by RequestMap.configAttribute in Spring Security Rest.
 * <p>
 * Spring Security expressions
 * <a href="https://grails.github.io/grails-spring-security-core/4.0.x/index.html">Spring Security Core</a>
 * Table 6. Spring Security expressions
 * Table 7. Traditional configurations and associated expressions
 * <p>
 * - hasRole(role): Returns true if the current principal has the specified role
 * - hasAnyRole([role1,role2]): Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings)
 * - principal: Allows direct access to the principal object representing the current user
 * - authentication: Allows direct access to the current Authentication object obtained from the SecurityContext
 * - permitAll: Always evaluates to true
 * - denyAll: Always evaluates to false
 * - isAnonymous(): Returns true if the current principal is an anonymous user
 * - isRememberMe(): Returns true if the current principal is a remember-me user
 * - isAuthenticated(): Returns true if the user is not anonymous
 * - isFullyAuthenticated(): Returns true if the user is not an anonymous or a remember-me user
 * - request: the HTTP request, allowing expressions such as “isFullyAuthenticated() or request.getMethod().equals('OPTIONS')”
 */
@RequiredArgsConstructor
@Component
public class RequestMapManager {
    public static final String URL_SINGLE_WILDCARD = "/*";
    public static final String URL_DOUBLE_WILDCARD = "/**";
    public static final String DENY_ALL = "denyAll";
    public static final String PERMIT_ALL = "permitAll";
    public static final String IS_AUTHENTICATED_PREFIX = "isAuthenticated";
    public static final String IS_AUTHENTICATED = IS_AUTHENTICATED_PREFIX + "()";
    public static final String IS_FULLY_AUTHENTICATED_PREFIX = "isFullyAuthenticated";
    public static final String IS_FULLY_AUTHENTICATED = IS_FULLY_AUTHENTICATED_PREFIX + "()";
    public static final String HAS_ROLE_PREFIX = "hasRole";
    public static final String HAS_ANY_ROLE_PREFIX = "hasAnyRole";
    public static final String HAS_NO_ROLE_PREFIX = "hasNoRole";
    public static final String HAS_NO_ANY_ROLE_PREFIX = "hasNoAnyRole";

    private static List<RequestMap> cachedRequestMaps;

    private RequestMapQuery requestMapQuery;

    public RequestMapManager(RequestMapQuery requestMapQuery) {
        this.requestMapQuery = requestMapQuery;
    }

    public List<RequestMap> getCachedRequestMaps() {
        if (cachedRequestMaps == null) {
            reloadCachedRequestMaps();
        }

        return cachedRequestMaps;
    }

    public void reloadCachedRequestMaps() {
        cachedRequestMaps = requestMapQuery.queryAll();
    }

    /**
     * Returns the expression like hasRole('ROLE_ADMIN').
     *
     * @param roleAuthority It is the string of role authority. AKA role name.
     */
    public static String hasRole(String roleAuthority) {
        return buildExpressionWithRole(HAS_ROLE_PREFIX, roleAuthority);
    }

    /**
     * Returns the expression like hasAnyRole('ROLE_USER','ROLE_ADMIN').
     *
     * @param roleAuthorities It is the list string of role authority. AKA role name.
     */
    public static String hasAnyRole(List<String> roleAuthorities) {
        return buildExpressionWithRoles(HAS_ANY_ROLE_PREFIX, roleAuthorities);
    }

    /**
     * Returns the expression like hasNoRole('ROLE_ADMIN').
     *
     * @param roleAuthority It is the string of role authority. AKA role name.
     */
    public static String hasNoRole(String roleAuthority) {
        return buildExpressionWithRole(HAS_NO_ROLE_PREFIX, roleAuthority);
    }

    /**
     * Returns the expression like hasNoAnyRole('ROLE_USER','ROLE_ADMIN').
     *
     * @param roleAuthorities It is the list string of role authority. AKA role name.
     */
    public static String hasNoAnyRole(List<String> roleAuthorities) {
        return buildExpressionWithRoles(HAS_NO_ANY_ROLE_PREFIX, roleAuthorities);
    }

    /**
     * Extracts role names from request map config attribute.
     * Example: hasRole('admin'), hasAnyRole('reporter', 'planner').
     *
     * @param configAttribute: Property configAttribute of request map.
     * @return String list of role name.
     */
    public List<String> extractRoleNames(String configAttribute) {
        List<String> roleNames = new ArrayList<>();

        // This would match the role names, like 'admin' or 'reporter', 'planner'.
        // Regular expression with string concatenation for the prefixes.
        String regex = "(?:"
                + RequestMapManager.HAS_ROLE_PREFIX + "|"
                + RequestMapManager.HAS_ANY_ROLE_PREFIX + "|"
                + RequestMapManager.HAS_NO_ROLE_PREFIX + "|"
                + RequestMapManager.HAS_NO_ANY_ROLE_PREFIX +
                ")\\((.+)\\)";

        // Create a Pattern object
        Pattern pattern = Pattern.compile(regex);

        // Create a matcher object
        Matcher matcher = pattern.matcher(configAttribute);

        // If match found.
        if (matcher.find()) {
            // Get the matched roles string (inside the parentheses)
            String matchedRoles = matcher.group(1);

            // Remove all single quotes
            matchedRoles = matchedRoles.replace("'", "");

            // Remove all double quotes
            matchedRoles = matchedRoles.replace("\"", "");

            // Split by comma
            String[] splitedRoles = matchedRoles.split(",");

            // Trim each role and add it to the roleNames list
            for (String role : splitedRoles) {
                roleNames.add(role.trim());
            }
        }

        return roleNames;
    }

    /**
     * Checks if security request map configAttribute contains any role in roleNames.
     *
     * @param roleNames:      String list of role names.
     * @param configAttribute String of config attribute. Example: hasAnyRole('ROLE_ACCOUNT_GROUP_USER','ROLE_ACCOUNT_GROUP_ADMIN')
     */
    public boolean containsAnyRoleInConfigAttribute(List<String> roleNames, String configAttribute) {
        return containsAnyRoleInConfigRoleNames(roleNames, extractRoleNames(configAttribute));
    }

    /**
     * Returns the expression like hasAnyRole('ROLE_USER','ROLE_ADMIN').
     *
     * @param prefix:         String of expression prefix. Like hasRole, hasNoRole.
     * @param roleAuthorities It is the list string of role authority. AKA role name.
     */
    private static String buildExpressionWithRoles(String prefix, List<String> roleAuthorities) {
        // Using StringBuilder for efficient string concatenation.
        StringBuilder result = new StringBuilder(prefix).append("(");

        // Appends each roleAuthority to result.
        for (String roleAuthority : roleAuthorities) {
            result.append("'").append(roleAuthority).append("',");
        }

        // Removes the last comma and close with a bracket
        if (!roleAuthorities.isEmpty()) {
            // Removes the trailing comma.
            result.setLength(result.length() - 1);
        }

        result.append(")");

        return result.toString();
    }

    /**
     * Returns the expression like hasRole('ROLE_ADMIN').
     *
     * @param prefix:       String of expression prefix. Like hasRole, hasNoRole.
     * @param roleAuthority It is the string of role authority. AKA role name.
     */
    private static String buildExpressionWithRole(String prefix, String roleAuthority) {
        return String.format("%s('%s')", prefix, roleAuthority);
    }

    /**
     * Checks if configuredRoleNames contains any role in roleNames.
     *
     * @param roleNames:       String list of role names.
     * @param configRoleNames: String list of configured role names.
     * @return True means condition meets. False means NOT.
     */
    private boolean containsAnyRoleInConfigRoleNames(List<String> roleNames, List<String> configRoleNames) {
        boolean containsAnyRole = false;

        for (String roleName : roleNames) {
            for (String configRoleName : configRoleNames) {
                if (roleName.equalsIgnoreCase(configRoleName)) {
                    containsAnyRole = true;
                    break;
                }
            }
        }

        return containsAnyRole;
    }
}
